Skip to content

✨ Add Rotation Gate Merging using Quaternions#1407

Open
J4MMlE wants to merge 31 commits intomunich-quantum-toolkit:mainfrom
J4MMlE:quaternion-rotation-merging
Open

✨ Add Rotation Gate Merging using Quaternions#1407
J4MMlE wants to merge 31 commits intomunich-quantum-toolkit:mainfrom
J4MMlE:quaternion-rotation-merging

Conversation

@J4MMlE
Copy link
Copy Markdown

@J4MMlE J4MMlE commented Dec 28, 2025

Description

This PR extends the rotation merging pass in the QCO MQTOpt dialect to support quaternion-based gate fusion.

The existing rotation merge pass only merges consecutive rotation gates of the same type (e.g., rx + rx or ry + ry) by adding their angles.
This PR introduces quaternion-based merging, which can merge rotation gates of different types (currently only single qubit gates rx, ry, rz, r, p, u2, u).

Quaternions are widely used to represent rotations in three-dimensional space and naturally map to qubit gate rotations around the Bloch sphere. The implementation:

  1. Converts rotation gates to quaternion representation
  2. Multiplies quaternions using the Hamilton product
  3. Converts the resulting quaternion back to a u gate. (This could also be done differently in the future, and directly decompose to the correct base gates by using the decomposition from ✨ Implement single-qubit gate decomposition pass #1182)

Since this optimization may only be beneficial on certain quantum architectures, it is disabled by default. It can be invoked using:

mqt-cc --mlir-merge-single-qubit-rotation-gates <input.mlir>

Closes #1029

Checklist:

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
    - [ ] I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

@codecov
Copy link
Copy Markdown

codecov bot commented Dec 28, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@burgholzer burgholzer added feature New feature or request c++ Anything related to C++ code MLIR Anything related to MLIR labels Dec 29, 2025
@burgholzer burgholzer added this to the MLIR Support milestone Dec 29, 2025
@burgholzer
Copy link
Copy Markdown
Member

Hey @J4MMlE 👋🏻
This is great to see!

How much of an ask would it be to directly base this pass on the QCO dialect and its infrastructure?
We'd like to stop adding features to the old dialects and instead only add them to the new ones as much as possible, so that we can remove the old dialects rather sooner than later.
This is not a must, but it would be highly appreciated.

@mergify mergify bot added the conflict label Jan 14, 2026
@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 106575c to 7528b05 Compare January 14, 2026 22:38
@mergify mergify bot removed the conflict label Jan 14, 2026
@burgholzer burgholzer requested a review from DRovara January 17, 2026 00:07
@burgholzer burgholzer linked an issue Jan 17, 2026 that may be closed by this pull request
Copy link
Copy Markdown
Collaborator

@DRovara DRovara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @J4MMlE for the effort! The code already looks super clean in my opinion and I like the way you have implemented everything.

I do have quite a few minor comments, but they are largely just related to comments in the code (docstrings/typos).

What I did not look at in much detail is the CMake setup. I guess it would make sense if @burgholzer (after your vacation, of course) could look into that - although probably it would be most efficient to do that only once the tests are ready, too (btw, I think the point from my top-level comment below might also be interesting to consider for you).

Anyways, @J4MMlE, once you have addressed the comments and added the tests, feel free to re-request my review and I will look at the code again. Thanks a lot in the meantime!

@mergify mergify bot added the conflict label Jan 20, 2026
@DRovara
Copy link
Copy Markdown
Collaborator

DRovara commented Jan 21, 2026

I just remembered one more comment I wanted to make that I forgot:

You talk about how you no longer have to explicitly filter for control qubits due to the new dialect structure.
However, right now, I believe your pass would not work at all with controlled gates anymore - I'm not sure if that's intended.

Imagine the following pseudo-code:

%q0_0, %q1_0 = alloc(2)
[...]
%q0_1, %q1_1 = qco.ctrl(%q0_0), (%q1c_0 = %q1_0) {
  %q1c_1 = qco.u(...) %q1c_0
  qco.yield %q1c_1 
}
%q0_2, %q1_2 = qco.ctrl(%q0_1), (%q1c2_0 = %q1_1) {
  %q1c2_1 = qco.u(...) %q1c2_0
  qco.yield %q1c2_1 
}

Here, the first u gate has qco.yield as its user. However, in short, the snippet above corresponds to:

controlled(q0) u(...) q1
controlled(q0) u(...) q1

so in theory, this can definitely be merged.

Now, I don't know if this is a flaw of the pass (maybe this situation should be checked explicitly), a flaw of the dialect implementation (maybe QCO should provide a way to get the actual successor gate, rather than the yield which we don't care much about), or if it's just out of scope for this pass.

My personal gut feeling is that it would be a nice helper method to implement for the QCO UnitaryOpInterface.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from aaa7096 to 94ea576 Compare January 21, 2026 19:33
@mergify mergify bot removed the conflict label Jan 21, 2026
@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from f0989ad to 045857a Compare January 21, 2026 19:57
Copy link
Copy Markdown
Collaborator

@DRovara DRovara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @J4MMlE for the work. It looks super good already (for the record, I still haven't looked at the CMake configuration, I'll leave that to someone else).
The tests also look really really clean, I like that a lot.

I did have a minor concern regarding the numerical correctness, maybe you could check that out real quick? Either I am wrong or something is not quite working correctly.

Once my comments are addressed, feel free to remove the "Draft" status from this PR and request a review from Lukas.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 042de31 to 290b626 Compare February 5, 2026 20:21
@J4MMlE J4MMlE marked this pull request as ready for review February 5, 2026 20:48
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Adds a quaternion-based MergeRotationGates MLIR pass, a QuantumCompilerConfig flag and CLI option to enable it, conditionally integrates the pass into the compiler pipeline, updates CMake linkages and test targets, and adds unit tests and a changelog entry.

Changes

Cohort / File(s) Summary
Config & Pass Declarations
mlir/include/mlir/Compiler/CompilerPipeline.h, mlir/include/mlir/Dialect/QCO/Transforms/Passes.td, mlir/include/mlir/Dialect/QCO/Transforms/Passes.h
Adds bool mergeSingleQubitRotationGates = false to QuantumCompilerConfig; introduces MergeRotationGates TableGen pass; removes a small comment block in Passes.h.
Optimization Implementation
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
New greedy rewrite pass merging consecutive rotation-like gates into one UOp via quaternion conversion, Hamilton product accumulation, quaternion→ZYZ extraction with gimbal-lock handling, SSA rewiring, and pass failure signaling on pattern application failure.
Pipeline Integration & CLI
mlir/lib/Compiler/CompilerPipeline.cpp, mlir/tools/mqt-cc/mqt-cc.cpp
Includes QCO pass header; Stage 5 conditionally adds createMergeRotationGates() when config_.mergeSingleQubitRotationGates is true; adds CLI flag --mlir-merge-single-qubit-rotation-gates wired into the compiler config.
Build System (Compiler & QCO)
mlir/lib/Compiler/CMakeLists.txt, mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt
Adds MLIRQCOTransforms to MQTCompilerPipeline public link libs; replaces ${dialect_libs} with explicit MLIRArithDialect and MLIRMathDialect in QCO transforms private links.
Unit Tests & Test CMake
mlir/unittests/Compiler/test_compiler_pipeline.cpp, mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt, mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp
Extends test harness to pass the merge flag; adds new test executable and extensive tests validating structural and numerical correctness of quaternion merging across many scenarios, including NaN/gimbal-lock checks.
Test Integration & Minor CMake Edits
mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt, mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt
Adds add_subdirectory(Optimizations) to include new tests; reorders some PRIVATE link libraries in an existing test CMake file.
Changelog
CHANGELOG.md
Adds Unreleased entry and PR/contributor reference for the new pass (PR #1407).

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    participant CLI as CLI (mqt-cc)
    end
    rect rgba(200,255,200,0.5)
    participant Pipeline as CompilerPipeline
    participant PM as PassManager
    participant QCO as MLIRQCOTransforms
    end

    CLI->>Pipeline: set QuantumCompilerConfig.mergeSingleQubitRotationGates
    Pipeline->>PM: build Stage 5 passes
    alt mergeSingleQubitRotationGates == true
        Pipeline->>PM: add MergeRotationGates pass
    end
    PM->>QCO: execute MergeRotationGates (pattern rewrites)
    QCO-->>PM: success / failure
    PM-->>Pipeline: continue remaining stages
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • burgholzer

Poem

🐰 I hop through quaternions, light and fleet,
I twirl two rotations till they meet.
Hamilton hums, angles find their way,
One neat U replaces the fray.
Hooray — small circuits, happy feet.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding quaternion-based rotation gate merging. It is concise, specific, and directly reflects the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 87.59% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description is mostly complete with a clear summary, motivation, technical implementation details, usage instructions, and most checklist items marked as complete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt`:
- Line 11: The global add_compile_options(-fexceptions) affects all targets;
instead remove that line and add a scoped compile option on the
MLIRQCOTransforms target by calling target_compile_options(MLIRQCOTransforms
PRIVATE -fexceptions) after the MLIRQCOTransforms target is created (e.g., after
the add_library/add_mlir_dialect or similar target definition) so only
MLIRQCOTransforms gets -fexceptions.
- Line 13: Remove the stray debug output in the CMakeLists by deleting or
guarding the message(STATUS "MLIR_DIALECT_LIBS contains: ${dialect_libs}") line;
either remove that message entirely from
mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt or wrap it behind a project-level
debug option (e.g., only call message when a DEBUG_CMAKE or similar variable is
enabled) so normal builds no longer emit the dialect_libs status line.

In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp`:
- Around line 363-370: Fix the comment typo on the line above the SelectOp
creations: change "weather" to "whether" in the sentence "// choose correct
alpha and gamma weather safe or not" so it reads "// choose correct alpha and
gamma whether safe or not" (locate the comment immediately preceding the
creation of alpha and gamma via rewriter.create<mlir::arith::SelectOp>).

In `@mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt`:
- Around line 11-20: Remove or replace the TODO in the target_link_libraries
block for mqt-core-mlir-dialect-qco-transforms-test: either delete the "# TODO
figure out correct dependencies" line or update it to a concrete status/note
(e.g., "Verified dependencies for CI" or list missing deps) so it no longer
suggests unfinished work; verify the linked targets (GTest::gtest_main,
MLIRQCOProgramBuilder, MLIRQCOTransforms, MLIRIR, MLIRPass, MLIRSupport,
LLVMSupport) are intentionally present before committing the change.

In `@mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp`:
- Around line 543-577: Move the SCF dialect load into the test fixture SetUp:
add context.loadDialect<scf::SCFDialect>(); inside the
QCOQuaternionMergeTest::SetUp() implementation so all tests consistently load
SCFDialect, and remove the duplicate context.loadDialect<scf::SCFDialect>();
call from the multipleUseInIf test (which currently calls it inside that test
body).
- Around line 136-153: The buildRotations function reads gate.angles[0..2] for
UOp without checking length, risking OOB; update buildRotations to validate the
RotationGate angles vector before calling builder.u by asserting or returning an
error when gate.opName == UOp::getOperationName() and gate.angles.size() < 3 (or
providing safe defaults), e.g. add a size check for gate.angles >= 3 and emit a
clear assertion/log message that includes the offending gate so the caller can
diagnose; reference buildRotations, RotationGate, UOp, gate.angles, and
builder.u when locating the change.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 290b626 to 6c5018e Compare February 6, 2026 12:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp`:
- Around line 136-144: Add a trailing llvm_unreachable() immediately after the
switch that handles RotationAxis to guard against future enum extensions and
silence non-exhaustive-switch warnings; locate the switch on axis (the one
returning quaternion literals for RotationAxis::X/Y/Z in
QuaternionMergeRotationGates.cpp) and append llvm_unreachable("Unhandled
RotationAxis") (and include the proper header <llvm/Support/ErrorHandling.h> if
not already included).
- Around line 309-316: The computed acos input bTemp3 (constructed from ww, zz,
bTemp1, bTemp2) can drift outside [-1,1]; clamp it to the valid domain before
calling mlir::math::AcosOp to avoid NaN. Replace the direct use of bTemp3 in the
AcosOp with a clamped value produced via mlir::arith::MinFOp and
mlir::arith::MaxFOp (or a combination) to bound it to [-1.0, 1.0] using the
existing constants (one, negOne or create negOne if needed); then pass that
clamped value to the creation of beta (rewriter.create<mlir::math::AcosOp>(loc,
clampedValue)). Ensure the new ops reference the same loc and use rewriter so
the IR stays consistent.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch 2 times, most recently from 4cb71da to c875d00 Compare February 8, 2026 22:12
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp`:
- Around line 374-375: The alpha/gamma extraction in
QuaternionMergeRotationGates.cpp can produce angles outside [-PI, PI]; implement
normalization for the variables alpha and gamma (e.g., in the code path that
computes these values inside the QuaternionMergeRotationGates transform/pass) by
mapping them into the principal range [-M_PI, M_PI] using a modular reduction
(fmod/remainder) and ±2*PI adjustment so mathematically equivalent angles are
canonical; update any uses of alpha/gamma in emit/replace logic within the
QuaternionMergeRotationGates routine to use the normalized values.

In `@mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp`:
- Around line 702-715: Update the test comment in TEST_F(QCOQuaternionMergeTest,
numericalRotationIdentity) to match the assertion in the test
(expectUGateParams(0, TAU, 0)) rather than claiming U(0, 0, 0) or U(PI, -PI, 0);
reference the actual expected output U(0, TAU, 0) (with TAU = 2π) in the comment
or alternatively change the assertion to expect U(0, 0, 0) if that is the
intended canonical form—locate the test by the name numericalRotationIdentity
and the call expectUGateParams(0, TAU, 0) to make the consistent update.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch 2 times, most recently from 8ff7ccc to f3cbe5b Compare February 8, 2026 22:18
@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from b73951e to 51fbbfa Compare March 21, 2026 23:15
@J4MMlE
Copy link
Copy Markdown
Author

J4MMlE commented Mar 21, 2026

@burgholzer its ready for review again!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (1)

591-605: ⚠️ Potential issue | 🟠 Major

Reintroduce an explicit insertion-point guard before building the merged chain.

All arith::/math:: ops for the merged quaternion are created at the current rewrite insertion point, which is still the chain head here. If a later gate angle is produced by an arith op between chain elements, the new ops will be inserted before that definition and violate SSA dominance. Please move the insertion point to chain.back().getOperation() (or just before the first post-chain consumer) while materializing constants, qAccum, and newOp.

🐛 Proposed fix
     auto chain = collectChain(op);
     if (chain.size() < 2) {
       return failure();
     }
 
+    OpBuilder::InsertionGuard guard(rewriter);
+    rewriter.setInsertionPoint(chain.back().getOperation());
+
     auto loc = op->getLoc();
     auto constants = createConstants(loc, rewriter);
 
     // Initialize quaternion accumulator from the first operation
     auto qAccum = quaternionFromRotation(chain.front(), constants, rewriter);

Run the following script to confirm the missing insertion-point adjustment and the lack of a regression test with late-defined scalar angles:

#!/bin/bash
set -euo pipefail

echo "=== matchAndRewrite body ==="
fd QuaternionMergeRotationGates.cpp --exec sed -n '580,610p' {}

echo
echo "=== quaternion-merge tests using arithmetic between mergeable gates ==="
fd test_qco_quaternion_merge.cpp --exec rg -n 'arith\.(addf|subf|mulf|divf)' {}

Expected result: matchAndRewrite shows no setInsertionPoint* call before building constants/qAccum/newOp, and the test search returns no coverage for a late-defined angle value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 591 - 605, The merged-quaternion ops are currently created at the
rewrite's existing insertion point (head of the chain), risking SSA dominance if
a later gate angle is produced by an arith op; before calling createConstants,
quaternionFromRotation (for the first op), the loop that folds with
hamiltonProduct, and before uOpFromQuaternion, set the rewriter insertion point
to chain.back().getOperation() (or the operation immediately before the first
post-chain consumer) so all arith::/math:: ops for the merged quaternion are
emitted after the chain elements; locate these calls (createConstants,
quaternionFromRotation, hamiltonProduct, uOpFromQuaternion) and wrap them with a
temporary insertion-point guard (setInsertionPoint / setInsertionPointAfter or
equivalent) and restore the original insertion point after newOp is created.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 555-568: Remove the stale pairwise helper by deleting the
createOpQuaternionMergedAngle function definition (and any forward declaration)
and any references to it so there is only the chain-based merge path
(matchAndRewrite) left; search for the symbol createOpQuaternionMergedAngle and
remove its declaration/definition from QuaternionMergeRotationGates.cpp and any
header, ensure no callers remain, remove any now-unused includes or using
directives, and run the build/static analysis to confirm the unused-function
warning is resolved.
- Around line 367-376: collectChain currently dereferences
current->getUsers().begin() before verifying there is a user, which causes UB
for a dead tail; update collectChain to first check that current->getUsers() is
non-empty (and ideally that it has exactly one user if that's required) before
dereferencing, then fetch the user, test
areQuaternionMergeable(*current.getOperation(), *userOp) and only then push/cast
and advance current; modify the loop around current, userOp and the
areQuaternionMergeable call so dereferencing never happens when getUsers() is
empty.

In `@mlir/tools/mqt-cc/mqt-cc.cpp`:
- Around line 74-77: The cl::opt option mergeSingleQubitRotationGates is
declared const but must follow the file convention of mutable command-line
options; remove the const and declare it as static cl::opt<bool>
mergeSingleQubitRotationGates(...) so ParseCommandLineOptions can update it and
the declaration matches other options in this file.

In `@mlir/unittests/Compiler/test_compiler_pipeline.cpp`:
- Around line 195-199: The test comment is inaccurate: it claims the test
compares outputs “with and without the pass enabled” but the code actually
compares afterQCOCanon and afterOptimization from a single run; update the
comment to accurately describe the current comparison logic (that the test
compares the optimization output stages after running the pass, specifically
comparing the afterQCOCanon and afterOptimization results) and remove or reword
the “with and without the pass enabled” phrasing so it matches the behavior
around afterQCOCanon and afterOptimization in test_compiler_pipeline.cpp.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`:
- Around line 503-517: The test currently uses two RX gates (builder.rx) which
don't prove cross-qubit safety because same-axis RX+RX are intentionally not
merged; change the second gate to a different mergeable type (e.g.,
builder.ry(1.0, qubit2) or a UOp) so the pass must respect qubit identity, then
update the assertions: keep invoking runMergePass(module.get()) and assert that
both separate rotations remain (e.g., EXPECT_EQ(countOps<RXOp>(), 1) and
EXPECT_EQ(countOps<RYOp>(), 1)) and/or assert that no UOp was produced
(EXPECT_EQ(countOps<UOp>(), 0)); target symbols:
QCOQuaternionMergeTest::dontMergeGatesFromDifferentQubits, builder.rx,
builder.ry (or UOp), runMergePass, countOps<RXOp>(), countOps<RYOp>(),
countOps<UOp>().

---

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 591-605: The merged-quaternion ops are currently created at the
rewrite's existing insertion point (head of the chain), risking SSA dominance if
a later gate angle is produced by an arith op; before calling createConstants,
quaternionFromRotation (for the first op), the loop that folds with
hamiltonProduct, and before uOpFromQuaternion, set the rewriter insertion point
to chain.back().getOperation() (or the operation immediately before the first
post-chain consumer) so all arith::/math:: ops for the merged quaternion are
emitted after the chain elements; locate these calls (createConstants,
quaternionFromRotation, hamiltonProduct, uOpFromQuaternion) and wrap them with a
temporary insertion-point guard (setInsertionPoint / setInsertionPointAfter or
equivalent) and restore the original insertion point after newOp is created.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac9eb884-6cfb-41bb-bec4-2be8b1a0698c

📥 Commits

Reviewing files that changed from the base of the PR and between 1a22b2b and b73951e.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • mlir/include/mlir/Compiler/CompilerPipeline.h
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Compiler/CMakeLists.txt
  • mlir/lib/Compiler/CompilerPipeline.cpp
  • mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (1)

555-568: 🧹 Nitpick | 🔵 Trivial

Remove the stale pairwise merge helper.

createOpQuaternionMergedAngle is no longer referenced after the chain-based rewrite. Keeping a second merge path here makes future fixes easy to miss, and Cppcheck is already flagging it as unused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 555 - 568, Remove the now-unused pairwise merge helper by deleting
the createOpQuaternionMergedAngle function definition (and any corresponding
forward declaration) and any code that references it; locate the function
signature using the symbols UnitaryOpInterface and PatternRewriter and the body
that calls quaternionFromRotation, hamiltonProduct, and uOpFromQuaternion, then
remove that entire helper to avoid a duplicate merge path and silence the
Cppcheck unused warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 597-604: qAccum may drift from unit length after repeated
hamiltonProduct calls (from quaternionFromRotation and hamiltonProduct) and
uOpFromQuaternion assumes a unit quaternion; add a normalize step before calling
uOpFromQuaternion: implement a helper normalizeQuaternion(Quaternion, Location,
const Constants&, PatternRewriter&) that computes sqrt(w*w+x*x+y*y+z*z), divides
each component by the norm (using math::SqrtOp and arithmetic ops), and replace
the direct use of qAccum with the normalized quaternion when invoking
uOpFromQuaternion.

---

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 555-568: Remove the now-unused pairwise merge helper by deleting
the createOpQuaternionMergedAngle function definition (and any corresponding
forward declaration) and any code that references it; locate the function
signature using the symbols UnitaryOpInterface and PatternRewriter and the body
that calls quaternionFromRotation, hamiltonProduct, and uOpFromQuaternion, then
remove that entire helper to avoid a duplicate merge path and silence the
Cppcheck unused warning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce48f6a1-6e52-4dad-a4aa-afd4a413b5f0

📥 Commits

Reviewing files that changed from the base of the PR and between b73951e and 51fbbfa.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 51fbbfa to b2a7d4b Compare March 21, 2026 23:58
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp (1)

132-141: ⚠️ Potential issue | 🟡 Minor

Compare U angles modulo periodicity to prevent false-negative tests.

expectUGateParams currently does direct numeric comparison, but these parameters are periodic; the identity test even documents two equivalent forms (phi = 0 or ) while asserting only one. This can fail on equivalent outputs.

♻️ Proposed fix
+  static double circularDiff(double a, double b, double period) {
+    const double diff = std::fmod(std::abs(a - b), period);
+    return std::min(diff, period - diff);
+  }
+
   void expectUGateParams(double expectedTheta, double expectedPhi,
                          double expectedLambda, double tolerance = 1e-8) {
     auto params = getUGateParams();
     ASSERT_TRUE(params.has_value());
 
     auto [theta, phi, lambda] = *params;
-    EXPECT_NEAR(theta, expectedTheta, tolerance);
-    EXPECT_NEAR(phi, expectedPhi, tolerance);
-    EXPECT_NEAR(lambda, expectedLambda, tolerance);
+    EXPECT_NEAR(circularDiff(theta, expectedTheta, 4.0 * PI), 0.0, tolerance);
+    EXPECT_NEAR(circularDiff(phi, expectedPhi, TAU), 0.0, tolerance);
+    EXPECT_NEAR(circularDiff(lambda, expectedLambda, TAU), 0.0, tolerance);
   }

Based on learnings: In MQT Core’s OpenQASM 2 U definition, theta is 4π periodic while phi and lambda are 2π periodic.

Also applies to: 741-754

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`
around lines 132 - 141, The test's expectUGateParams does direct numeric
comparisons causing false negatives for equivalent U gates; update
expectUGateParams (and the other similar assertions around the second block
referencing U parameters) to compare angles modulo periodicity: normalize theta
differences modulo 4π and phi/lambda modulo 2π (e.g., compute minimal
wrap-around difference between expected and actual angle and then use
EXPECT_NEAR on that minimal difference with the given tolerance); locate
getUGateParams usage and replace the three EXPECT_NEAR checks with
wrapped-comparison logic so equivalent angles like 0 vs 2π (or theta differing
by 4π) no longer fail.
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (3)

601-608: ⚠️ Potential issue | 🟠 Major

Renormalize qAccum before converting back to Euler angles.

uOpFromQuaternion assumes a unit quaternion, but this path now chains several Hamilton products without the old quaternion→Euler→quaternion reset. Floating-point drift in qAccum will skew beta, and near the gimbal-lock thresholds it can also flip the safe branch. Normalize once before Line 608.

➕ Suggested fix
   for (auto chainOp : llvm::drop_begin(chain)) {
     auto qi = quaternionFromRotation(chainOp, constants, rewriter);
     qAccum = hamiltonProduct(qi, qAccum, loc, rewriter);
   }
+  qAccum = normalizeQuaternion(qAccum, loc, constants, rewriter);

   // Convert merged quaternion back to UOp
   auto newOp = uOpFromQuaternion(qAccum, op, constants, rewriter);
static Quaternion normalizeQuaternion(Quaternion q, Location loc,
                                      const Constants& constants,
                                      PatternRewriter& rewriter) {
  auto ww = arith::MulFOp::create(rewriter, loc, q.w, q.w);
  auto xx = arith::MulFOp::create(rewriter, loc, q.x, q.x);
  auto yy = arith::MulFOp::create(rewriter, loc, q.y, q.y);
  auto zz = arith::MulFOp::create(rewriter, loc, q.z, q.z);

  auto sum1 = arith::AddFOp::create(rewriter, loc, ww, xx);
  auto sum2 = arith::AddFOp::create(rewriter, loc, yy, zz);
  auto normSq = arith::AddFOp::create(rewriter, loc, sum1, sum2);
  auto norm = math::SqrtOp::create(rewriter, loc, normSq);
  auto invNorm = arith::DivFOp::create(rewriter, loc, constants.one, norm);

  return {
      .w = arith::MulFOp::create(rewriter, loc, q.w, invNorm),
      .x = arith::MulFOp::create(rewriter, loc, q.x, invNorm),
      .y = arith::MulFOp::create(rewriter, loc, q.y, invNorm),
      .z = arith::MulFOp::create(rewriter, loc, q.z, invNorm),
  };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 601 - 608, Normalize qAccum before converting back to Euler angles:
insert a call to a new helper (e.g., normalizeQuaternion) that takes the
Quaternion qAccum, Location loc, Constants& constants, and PatternRewriter&
rewriter, computes its magnitude via w^2+x^2+y^2+z^2, sqrt, and multiplies each
component by 1/norm to produce a unit quaternion, then pass the normalized
result into uOpFromQuaternion instead of raw qAccum; implement
normalizeQuaternion returning a Quaternion and call it just before the
uOpFromQuaternion(qAccum, op, constants, rewriter) invocation.

595-608: ⚠️ Potential issue | 🔴 Critical

Build the merged quaternion where every later parameter dominates.

Line 603 can consume angle SSA values from gates that appear after chain.front(), but the current insertion point is still at the chain head. That emits arith/math ops using non-dominating operands as soon as a later rotation angle is computed between the head and tail, which makes the rewritten IR invalid. Move the whole merge computation under an insertion guard to a point before chain.back().

🛠️ Suggested fix
   auto loc = op->getLoc();
-  auto constants = createConstants(loc, rewriter);
+  OpBuilder::InsertionGuard guard(rewriter);
+  rewriter.setInsertionPoint(chain.back().getOperation());
+  auto constants = createConstants(loc, rewriter);

   // Initialize quaternion accumulator from the first operation
   auto qAccum = quaternionFromRotation(chain.front(), constants, rewriter);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 595 - 608, The merge loop computes and emits arithmetic using SSA
values from later gates while the rewriter insertion point is still at the chain
head; move the entire quaternion-merge computation (creation of constants,
quaternionFromRotation calls, hamiltonProduct folding, and uOpFromQuaternion)
under an insertion-point guard so all new ops are inserted at a location that
dominates all consumed angle SSA values — specifically set the rewriter
insertion point to just before chain.back() (or use a
ScopedInsertionPoint/rewriter.setInsertionPoint before chain.back()) before
calling createConstants, quaternionFromRotation, hamiltonProduct, and
uOpFromQuaternion to ensure later parameters dominate.

547-572: 🧹 Nitpick | 🔵 Trivial

Remove the stale pairwise merge helper.

createOpQuaternionMergedAngle no longer has callers after the chain-based rewrite landed, so it is now dead code and a second merge path that can drift from matchAndRewrite. Please delete it instead of maintaining both implementations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 547 - 572, Delete the now-dead pairwise merge helper function
createOpQuaternionMergedAngle and any forward declarations or declarations
referring to it so only the chain-based merge remains; remove the entire static
function body (and any comments immediately attached) from
QuaternionMergeRotationGates.cpp, search for and eliminate any remaining
references to createOpQuaternionMergedAngle (including tests or headers) so
compilation still succeeds and matchAndRewrite remains the single merge
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 601-608: Normalize qAccum before converting back to Euler angles:
insert a call to a new helper (e.g., normalizeQuaternion) that takes the
Quaternion qAccum, Location loc, Constants& constants, and PatternRewriter&
rewriter, computes its magnitude via w^2+x^2+y^2+z^2, sqrt, and multiplies each
component by 1/norm to produce a unit quaternion, then pass the normalized
result into uOpFromQuaternion instead of raw qAccum; implement
normalizeQuaternion returning a Quaternion and call it just before the
uOpFromQuaternion(qAccum, op, constants, rewriter) invocation.
- Around line 595-608: The merge loop computes and emits arithmetic using SSA
values from later gates while the rewriter insertion point is still at the chain
head; move the entire quaternion-merge computation (creation of constants,
quaternionFromRotation calls, hamiltonProduct folding, and uOpFromQuaternion)
under an insertion-point guard so all new ops are inserted at a location that
dominates all consumed angle SSA values — specifically set the rewriter
insertion point to just before chain.back() (or use a
ScopedInsertionPoint/rewriter.setInsertionPoint before chain.back()) before
calling createConstants, quaternionFromRotation, hamiltonProduct, and
uOpFromQuaternion to ensure later parameters dominate.
- Around line 547-572: Delete the now-dead pairwise merge helper function
createOpQuaternionMergedAngle and any forward declarations or declarations
referring to it so only the chain-based merge remains; remove the entire static
function body (and any comments immediately attached) from
QuaternionMergeRotationGates.cpp, search for and eliminate any remaining
references to createOpQuaternionMergedAngle (including tests or headers) so
compilation still succeeds and matchAndRewrite remains the single merge
implementation.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`:
- Around line 132-141: The test's expectUGateParams does direct numeric
comparisons causing false negatives for equivalent U gates; update
expectUGateParams (and the other similar assertions around the second block
referencing U parameters) to compare angles modulo periodicity: normalize theta
differences modulo 4π and phi/lambda modulo 2π (e.g., compute minimal
wrap-around difference between expected and actual angle and then use
EXPECT_NEAR on that minimal difference with the given tolerance); locate
getUGateParams usage and replace the three EXPECT_NEAR checks with
wrapped-comparison logic so equivalent angles like 0 vs 2π (or theta differing
by 4π) no longer fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f5ced74e-9e8e-46f7-bb09-8a1038b2d2fe

📥 Commits

Reviewing files that changed from the base of the PR and between 51fbbfa and b2a7d4b.

📒 Files selected for processing (4)
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (1)

574-581: 🧹 Nitpick | 🔵 Trivial

Consider renormalizing the accumulated quaternion before Euler extraction.

After chaining multiple Hamilton products (lines 575-578), floating-point drift can move |qAccum| slightly off 1.0. While the acos input clamping at lines 481-484 partially mitigates this for beta, explicit renormalization would provide additional robustness for longer chains:

// After the Hamilton product loop:
// qAccum = normalizeQuaternion(qAccum, loc, constants, rewriter);

This is a minor robustness enhancement rather than a correctness bug—for typical chain lengths (2-5 gates), the accumulated error is negligible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 574 - 581, After accumulating quaternions with
quaternionFromRotation and hamiltonProduct into qAccum, normalize qAccum before
converting back to a UOp to avoid FP drift: insert a call to a quaternion
normalization helper (e.g., normalizeQuaternion that computes the quaternion
norm and divides components using constants/rewriter and loc) immediately after
the Hamilton product loop and before uOpFromQuaternion(qAccum, op, constants,
rewriter); if no normalizeQuaternion exists, add one that uses the existing
constants/rewriter pattern to compute 1.0 / sqrt(sumsq) and multiply qAccum
components by that scalar.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 374-380: The while loop body with the single-statement sequence
that gets the user and checks mergeability (using current, userOp,
areQuaternionMergeable, chain and UnitaryOpInterface) lacks braces; add braces
around the entire body of the while loop so the block that computes userOp,
tests areQuaternionMergeable(*current.getOperation(), *userOp), pushes to chain,
and updates current is enclosed in { ... } for consistent style and to avoid
future accidental errors.
- Around line 608-627: Move the MergeRotationGates pass struct into the existing
anonymous namespace to give it internal linkage: relocate the definition of
struct MergeRotationGates (which inherits
impl::MergeRotationGatesBase<MergeRotationGates>) inside the file's anonymous
namespace and keep its override of runOnOperation unchanged; ensure
populateMergeRotationGatesPatterns remains accessible to the pass (either by
leaving that function in the anonymous namespace or converting it to a method)
so the call inside runOnOperation continues to compile.
- Around line 15-25: The build may rely on a transitive include for
llvm::drop_begin used in QuaternionMergeRotationGates.cpp; add an explicit
include of the header that defines it by importing <llvm/ADT/STLExtras.h> at the
top of the file so references to llvm::drop_begin(chain) are resolved directly
(look for usages of llvm::drop_begin and the file
QuaternionMergeRotationGates.cpp to place the new include alongside the other
llvm includes).

---

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 574-581: After accumulating quaternions with
quaternionFromRotation and hamiltonProduct into qAccum, normalize qAccum before
converting back to a UOp to avoid FP drift: insert a call to a quaternion
normalization helper (e.g., normalizeQuaternion that computes the quaternion
norm and divides components using constants/rewriter and loc) immediately after
the Hamilton product loop and before uOpFromQuaternion(qAccum, op, constants,
rewriter); if no normalizeQuaternion exists, add one that uses the existing
constants/rewriter pattern to compute 1.0 / sqrt(sumsq) and multiply qAccum
components by that scalar.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73653c15-2a0d-4626-9811-68c8d547c584

📥 Commits

Reviewing files that changed from the base of the PR and between b2a7d4b and f889a11.

📒 Files selected for processing (1)
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp

@burgholzer
Copy link
Copy Markdown
Member

@burgholzer its ready for review again!

Could you please first address the linter warnings as well as the open CodeRabbit comments?

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (2)

571-580: ⚠️ Potential issue | 🟠 Major

Renormalize qAccum before converting it back to Euler angles.

uOpFromQuaternion assumes a unit quaternion, but Lines 574-577 now fold an arbitrary-length Hamilton-product chain without reprojecting onto the unit sphere. That drift skews beta and can flip the gimbal-lock branch near 0/PI. A small normalizeQuaternion(...) helper that divides by sqrt(w*w + x*x + y*y + z*z) is enough here.

♻️ Proposed fix
     for (auto chainOp : llvm::drop_begin(chain)) {
       auto qi = quaternionFromRotation(chainOp, constants, rewriter);
       qAccum = hamiltonProduct(qi, qAccum, loc, rewriter);
     }
+    qAccum = normalizeQuaternion(qAccum, loc, constants, rewriter);
 
     // Convert merged quaternion back to UOp
     auto newOp = uOpFromQuaternion(qAccum, op, constants, rewriter);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 571 - 580, After folding the chain into qAccum via
quaternionFromRotation and hamiltonProduct, renormalize qAccum before converting
back to Euler with uOpFromQuaternion: add or call a normalizeQuaternion(qAccum)
helper that divides each component by sqrt(w*w + x*x + y*y + z*z) (and
defensively handle zero/nan norm), then pass the normalized quaternion to
uOpFromQuaternion to avoid drift and gimbal-lock flips.

567-580: ⚠️ Potential issue | 🔴 Critical

Move the rewrite insertion point to the chain tail before building the quaternion IR.

Line 568 still creates the helper arith/math ops at the pattern’s current insertion point. If a later gate in the chain gets its angle from an SSA value defined between the head and tail, those new ops will reference that value before it dominates, and the rewrite emits invalid IR. Use an InsertionGuard and set the insertion point to chain.back().getOperation() before createConstants, quaternionFromRotation, and uOpFromQuaternion, then add a regression where the second angle is produced by an arith.addf between two mergeable gates.

♻️ Proposed fix
   auto loc = op->getLoc();
+  OpBuilder::InsertionGuard guard(rewriter);
+  rewriter.setInsertionPoint(chain.back().getOperation());
   auto constants = createConstants(loc, rewriter);
#!/bin/bash
echo "=== matchAndRewrite insertion point ==="
sed -n '556,586p' mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
echo
echo "=== current test coverage for SSA-computed angle values ==="
rg -n "arith\\.(addf|subf|mulf|divf)|builder\\.(rx|ry|rz|p|r|u2|u)\\(" mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 567 - 580, The rewrite currently creates helper arith/math ops at
the pattern's original insertion point, which can break SSA dominance; wrap the
rewrite with an mlir::OpBuilder::InsertionGuard and call
rewriter.setInsertionPointAfter(chain.back().getOperation()) (or
setInsertionPoint(chain.back().getOperation())) before calling createConstants,
quaternionFromRotation, and uOpFromQuaternion so all helper ops are emitted at
the chain tail; update the code locations around
qAccum/quaternionFromRotation/hamiltonProduct/uOpFromQuaternion to use the
guarded inserter and add a unit test where the second gate's angle is computed
via an arith.addf between two mergeable gates to verify correct dominance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 571-580: After folding the chain into qAccum via
quaternionFromRotation and hamiltonProduct, renormalize qAccum before converting
back to Euler with uOpFromQuaternion: add or call a normalizeQuaternion(qAccum)
helper that divides each component by sqrt(w*w + x*x + y*y + z*z) (and
defensively handle zero/nan norm), then pass the normalized quaternion to
uOpFromQuaternion to avoid drift and gimbal-lock flips.
- Around line 567-580: The rewrite currently creates helper arith/math ops at
the pattern's original insertion point, which can break SSA dominance; wrap the
rewrite with an mlir::OpBuilder::InsertionGuard and call
rewriter.setInsertionPointAfter(chain.back().getOperation()) (or
setInsertionPoint(chain.back().getOperation())) before calling createConstants,
quaternionFromRotation, and uOpFromQuaternion so all helper ops are emitted at
the chain tail; update the code locations around
qAccum/quaternionFromRotation/hamiltonProduct/uOpFromQuaternion to use the
guarded inserter and add a unit test where the second gate's angle is computed
via an arith.addf between two mergeable gates to verify correct dominance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0bacb27c-224d-4f9d-9129-a39f77fba6e8

📥 Commits

Reviewing files that changed from the base of the PR and between f889a11 and 114392b.

📒 Files selected for processing (2)
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 205-225: Update the docblocks for quaternionFromZYZ and the
UOp/ZYZ helper functions to explicitly state that the quaternion computed
represents the ZYZ decomposition only up to a global phase relative to the
OpenQASM2 U(theta,phi,lambda) matrix; i.e., call out that the quaternionization
is exact modulo a global phase and therefore not phase-sensitive, and add the
same caveat to the other helper docblocks mentioned (the other ZYZ/UOp helpers).
Ensure the wording names the functions (quaternionFromZYZ and the UOp/ZYZ
helpers) so future users know these helpers are not suitable for phase-sensitive
contexts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e6034b88-8205-45e5-8b89-2146cc3d279d

📥 Commits

Reviewing files that changed from the base of the PR and between 114392b and 5fb5edc.

📒 Files selected for processing (1)
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 5fb5edc to e606175 Compare March 22, 2026 15:17
};

/**
* @brief Checks if an operation is a mergeable rotation gate.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function already is pretty self-explanatory so I don't know if such a long docstring is justified. Might delete...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's where the shorthand doxygen syntax comes in. In cases like these you can simply use /// comments, e.g., in this case

/// Returns whether an operation is considered mergeable

@brief is implied for cases like these.


protected:
void runOnOperation() override {
// Get the current operation being operated on.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should default MLIR boilerplate like this have comments? Delete?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the getOperation is explicit enough and conveys what is going on. Comments like these can generally be removed in my opinion.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from e606175 to 212605a Compare March 22, 2026 21:54
@J4MMlE
Copy link
Copy Markdown
Author

J4MMlE commented Mar 23, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (2)

193-201: ⚠️ Potential issue | 🟡 Minor

Restore the trailing llvm_unreachable() in createAxisQuaternion.

The switch is exhaustive for today's enum, but this function still has a no-return path. That can reintroduce -Wreturn-type noise on some compilers and becomes UB if RotationAxis ever grows.

♻️ Suggested fix
     case RotationAxis::Z:
       return {.w = cos, .x = constants.zero, .y = constants.zero, .z = sin};
     }
+    llvm_unreachable("Unhandled RotationAxis");
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 193 - 201, The switch in createAxisQuaternion returns for all
current RotationAxis values but leaves a theoretical no-return path; restore a
final llvm_unreachable() (or equivalent assert) after the switch in
createAxisQuaternion to silence -Wreturn-type and prevent UB if RotationAxis is
extended in the future, ensuring the function always has a terminal unreachable
statement for the default/unhandled case.

85-94: 🧹 Nitpick | 🔵 Trivial

Use MLIR RTTI instead of getName() for the mergeability check.

This predicate is defining chain boundaries, so coupling it to textual op names makes it more brittle than the isa/TypeSwitch style you already use everywhere else in this file.

mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp (1)

125-137: ⚠️ Potential issue | 🟡 Minor

Compare U parameters modulo their natural periods in the helper.

expectUGateParams currently treats equivalent outputs like 0 vs or π vs as different, so a mathematically correct rewrite can fail these tests after harmless canonicalization changes.

♻️ Suggested fix
+  static double periodicDiff(double actual, double expected, double period) {
+    return std::abs(std::remainder(actual - expected, period));
+  }
+
   void expectUGateParams(double expectedTheta, double expectedPhi,
                          double expectedLambda, double tolerance = 1e-8) {
     auto params = getUGateParams();
     ASSERT_TRUE(params.has_value());
 
     auto [theta, phi, lambda] = *params;
-    EXPECT_NEAR(theta, expectedTheta, tolerance);
-    EXPECT_NEAR(phi, expectedPhi, tolerance);
-    EXPECT_NEAR(lambda, expectedLambda, tolerance);
+    EXPECT_NEAR(periodicDiff(theta, expectedTheta, 4.0 * PI), 0.0, tolerance);
+    EXPECT_NEAR(periodicDiff(phi, expectedPhi, 2.0 * PI), 0.0, tolerance);
+    EXPECT_NEAR(periodicDiff(lambda, expectedLambda, 2.0 * PI), 0.0,
+                tolerance);
   }

Based on learnings, in MQT Core the U gate uses the OpenQASM 2 definition, so θ is 4π-periodic while φ and λ are 2π-periodic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`
around lines 125 - 137, The helper expectUGateParams should compare angles
modulo their natural periods instead of raw values: retrieve params via
getUGateParams(), unwrap the optional as done now, then normalize theta to the
canonical range modulo 4π and normalize phi and lambda modulo 2π (e.g., map to
[-period/2, period/2] or [0,period) consistently) before calling EXPECT_NEAR;
update expectUGateParams to perform these modular reductions so equivalent
angles like 0 vs 2π or π vs -π are treated as equal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 193-201: The switch in createAxisQuaternion returns for all
current RotationAxis values but leaves a theoretical no-return path; restore a
final llvm_unreachable() (or equivalent assert) after the switch in
createAxisQuaternion to silence -Wreturn-type and prevent UB if RotationAxis is
extended in the future, ensuring the function always has a terminal unreachable
statement for the default/unhandled case.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`:
- Around line 125-137: The helper expectUGateParams should compare angles modulo
their natural periods instead of raw values: retrieve params via
getUGateParams(), unwrap the optional as done now, then normalize theta to the
canonical range modulo 4π and normalize phi and lambda modulo 2π (e.g., map to
[-period/2, period/2] or [0,period) consistently) before calling EXPECT_NEAR;
update expectUGateParams to perform these modular reductions so equivalent
angles like 0 vs 2π or π vs -π are treated as equal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03f31963-5f48-44c3-9b5d-066d5d834544

📥 Commits

Reviewing files that changed from the base of the PR and between 114392b and 212605a.

📒 Files selected for processing (2)
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

Copy link
Copy Markdown
Member

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @J4MMlE for another iteration 🙏🏼
We are getting there.
There is one bigger point in this review, which is the preservation of the global phase; all other comments are pretty minor and should be relatively straight-forward to address.

@DRovara maybe you also have an opinion on some of them.

bool printIRAfterAllStages = false;

/// Enable quaternion-based rotation gate merging
bool mergeSingleQubitRotationGates = false;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like this should default to true. There is no real downside of running this, right? We just need to make sure to be careful when this pass is run in relation to gate synthesis in general.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may need a refresher, but doesn't this pass also do stuff like merging RX; RZ? In that case, I'm not sure if we always want that. Sometimes, one wants to specifically work with special types of rotations (e.g., because the hardware only accepts those).

One could argue that this IR should be device agnostic, so it shouldn't matter. I'm just not sure if we maybe would then lose some information or potential for reductions when going back from U to RX; RZ.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It all depends on where this pass is being run in the pipeline. For the hardware-agnostic stage, I would argue it should be on by default. We do not have a hardware-dependent level; but this pass should not be run after translating to the native gate-set.

static cl::opt<bool> mergeSingleQubitRotationGates(
"mlir-merge-single-qubit-rotation-gates",
cl::desc("Enable quaternion-based single-qubit rotation gate merging"),
cl::init(false));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cl::init(false));
cl::init(true));

based on the other comment. But then again, it feels a bit weird to use this option to disable the pass.
I would almost argue that we could unconditionally run the pass as part of the compiler. That would also simplify a couple of other places here.

Comment on lines +196 to +198
* that is not bit-identical across platforms, so we cannot use
* verifyAllStages with hardcoded expected values. Instead, we run the
* pipeline once with the pass enabled and compare afterQCOCanon against
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still true? I think when I worked on the extended test suite, I enabled a fuzzy/approximate comparison for floating point values. But maybe I did not extend this to angle parameters of gates.


### Added

- ✨ Add a `mlir-merge-single-qubit-rotation-gates` pass for merging consecutive rotation gates using quaternions ([#1407]) ([**@J4MMlE**])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- ✨ Add a `mlir-merge-single-qubit-rotation-gates` pass for merging consecutive rotation gates using quaternions ([#1407]) ([**@J4MMlE**])
- ✨ Add a `merge-single-qubit-rotation-gates` pass for merging consecutive rotation gates using quaternions ([#1407]) ([**@J4MMlE**])

Just noticed this here when being contrasted to the place-and-route pass below; do we want to drop the mlir- prefix for the pass name?
Or, to open up another box, would it make sense to define our own prefix that identifies passes being run as part of mqt-cc? (e.g., by using the mqt-cc- prefix)
@munich-quantum-toolkit/mqt-cc any opinions on that?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally drop the mlir (make mlir an identifier for options/concepts taken directly from MLIR). I don't have a super strong opinion on the mqt-cc- prefix, except that it's a bit weird to call the program using

./mqt-cc test.mlir --mqt-cc-option1 --mqt-cc-option2...

One could say "well of course it's an mqt-cc option, I literally use it on mqt-cc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. Should be safe to just drop then and use no prefix.

Comment on lines +19 to +22
let description = [{
Merges consecutive rotation gates of different types (rx, ry, rz, p, r, u2, u)
using quaternion representation and the Hamilton product.
}];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is the documentation that also shows up in the API docs, this should be a bit more extensive and give more details on what is actually going on internally, including references. You can take the place-and-route pass docs as an example

Comment on lines +377 to +379
* Walks forward via single-use SSA edges. Breaks when the next operation is
* not mergeable or would form a same-type single-parameter pair with the
* current tail (leaving those for canonicalization).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To affirm the point from above: I think it creates cleaner code if we drop this limitation that relies on canonicalization.

collectChain(UnitaryOpInterface start) {
SmallVector<UnitaryOpInterface> chain = {start};
auto current = start;
while (!current->use_empty()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be wrong, but this condition will never actually trigger, right?
There will always be a user (due to linear types) and the condition that makes the loop stop is that a non-mergeable operation is encountered.
Hence, the logic can be simplified here.

if (!areQuaternionMergeable(*current.getOperation(), *userOp)) {
break;
}
chain.push_back(cast<UnitaryOpInterface>(userOp));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
chain.push_back(cast<UnitaryOpInterface>(userOp));
chain.emplace_back(cast<UnitaryOpInterface>(userOp));

And I am also sure that emplace_back returns a reference to the inserted element, so this can be combined with the next line.

Comment on lines +600 to +607
// Convert merged quaternion back to UOp
auto newOp = uOpFromQuaternion(qAccum, op, constants, rewriter);

// Bypass and erase each tail op, then replace the head with the merged UOp
for (auto chainOp : llvm::drop_begin(chain)) {
rewriter.replaceOp(chainOp, chainOp.getInputQubit(0));
}
rewriter.replaceOp(chain.front(), newOp);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have hoped that this can be made a bit cleaner.
I believe uOpFromQuaternion should simply return the three angle values and should not create the UOp operation. This should be created as part of the rewrite sequence in this part of the code.

The thing I am not so sure about: Wouldn't it be enough to simply replace the first operation of the chain and set the input of the first operation after the chain to the output of the new operation?
This essentially unlinks the entire chain of old operations and makes them unused.
Or does MLIR generally explicitly erase all operations and does not rely on dead value cleanup?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still dont understand why we should move UOp::create here. Could you elaborate on that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't move the UOp::create here, I would substitute it with a rewriter.replaceOp that directly constructs the operation in place instead of first constructing and then relinking the operation, which should be more efficient overall


protected:
void runOnOperation() override {
// Get the current operation being operated on.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the getOperation is explicit enough and conveys what is going on. Comments like these can generally be removed in my opinion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ MLIR - Support merging of more complex gates

4 participants